Explorez les nuances de l'optimisation des rappels ref de React. Découvrez pourquoi elle se déclenche deux fois, comment l'empêcher avec useCallback, et maîtrisez les performances pour les applications complexes.
Maîtriser les fonctions de rappel Ref de React : Le guide ultime pour l'optimisation des performances
Dans le monde du développement web moderne, la performance n'est pas seulement une fonctionnalité ; c'est une nécessité. Pour les développeurs utilisant React, la création d'interfaces utilisateur rapides et réactives est un objectif primordial. Bien que le DOM virtuel de React et l'algorithme de réconciliation gèrent une grande partie du travail difficile, il existe des modèles et des API spécifiques où une compréhension approfondie est cruciale pour débloquer des performances optimales. L'un de ces domaines est la gestion des refs, en particulier, le comportement souvent mal compris des rappels ref.
Les refs fournissent un moyen d'accéder aux nœuds DOM ou aux éléments React créés dans la méthode de rendu - une échappatoire essentielle pour des tâches telles que la gestion du focus, le déclenchement d'animations ou l'intégration avec des bibliothèques DOM tierces. Alors que useRef est devenu la norme pour les cas simples dans les composants fonctionnels, les rappels ref offrent un contrôle plus puissant et plus précis sur le moment où une référence est définie et supprimée. Cependant, cette puissance s'accompagne d'une subtilité : un rappel ref peut se déclencher plusieurs fois pendant le cycle de vie d'un composant, ce qui peut entraîner des goulots d'étranglement de performance et des bugs s'il n'est pas géré correctement.
Ce guide complet démystifiera le rappel ref de React. Nous explorerons :
- Ce que sont les rappels ref et comment ils diffèrent des autres types de ref.
- La raison principale pour laquelle les rappels ref sont appelés deux fois (une fois avec
nullet une fois avec l'élément). - Les pièges de performance liés à l'utilisation de fonctions en ligne pour les rappels ref.
- La solution définitive pour l'optimisation à l'aide du hook
useCallback. - Les modèles avancés pour la gestion des dépendances et l'intégration avec des bibliothèques externes.
À la fin de cet article, vous aurez les connaissances nécessaires pour manier les rappels ref avec confiance, en vous assurant que vos applications React sont non seulement robustes, mais aussi très performantes.
Un bref rappel : que sont les rappels Ref ?
Avant de nous plonger dans l'optimisation, revoyons brièvement ce qu'est un rappel ref. Au lieu de passer un objet ref créé par useRef() ou React.createRef(), vous passez une fonction à l'attribut ref. Cette fonction est exécutée par React lorsque le composant est monté et démonté.
React appellera le rappel ref avec l'élément DOM comme argument lorsque le composant est monté, et il l'appellera avec null comme argument lorsque le composant est démonté. Cela vous donne un contrôle précis aux moments exacts où la référence devient disponible ou est sur le point d'être détruite.
Voici un exemple simple dans un composant fonctionnel :
import React, { useState } from 'react';
function TextInputWithFocusButton() {
let textInput = null;
const setTextInputRef = element => {
console.log('Ref callback fired with:', element);
textInput = element;
};
const focusTextInput = () => {
// Focus the text input using the raw DOM API
if (textInput) textInput.focus();
};
return (
<div>
<input type="text" ref={setTextInputRef} />
<button onClick={focusTextInput}>
Focus the text input
</button>
</div>
);
}
Dans cet exemple, setTextInputRef est notre rappel ref. Il sera appelé avec l'élément <input> lorsqu'il sera rendu, ce qui nous permettra de le stocker et de l'utiliser plus tard pour appeler focus().
Le problème principal : pourquoi les rappels Ref se déclenchent-ils deux fois ?
Le comportement central qui déroute souvent les développeurs est la double invocation du rappel. Lorsqu'un composant avec un rappel ref est rendu, la fonction de rappel est généralement appelée deux fois de suite :
- Premier appel : avec
nullcomme argument. - Deuxième appel : avec l'instance de l'élément DOM comme argument.
Ce n'est pas un bug ; c'est un choix de conception délibéré de l'équipe React. L'appel avec null signifie que la ref précédente (le cas échéant) est en train d'être détachée. Cela vous donne une occasion cruciale d'effectuer des opérations de nettoyage. Par exemple, si vous avez attaché un écouteur d'événements au nœud lors du rendu précédent, l'appel null est le moment idéal pour le supprimer avant que le nouveau nœud ne soit attaché.
Le problème, cependant, n'est pas ce cycle de montage/démontage. Le véritable problème de performance survient lorsque ce double déclenchement se produit à chaque nouveau rendu, même lorsque l'état du composant se met à jour d'une manière totalement indépendante de la ref elle-même.
Le piège des fonctions en ligne
Considérez cette implémentation apparemment innocente à l'intérieur d'un composant fonctionnel qui est rendu à nouveau :
import React, { useState } from 'react';
function FrequentUpdatesComponent() {
const [count, setCount] = useState(0);
return (
<div>
<h3>Counter: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<div
ref={(node) => {
// This is an inline function!
console.log('Ref callback fired with:', node);
}}
>
I am the referenced element.
</div>
</div>
);
}
Si vous exécutez ce code et cliquez sur le bouton "Increment", vous verrez ce qui suit dans votre console à chaque clic :
Ref callback fired with: null
Ref callback fired with: <div>...</div>
Pourquoi cela se produit-il ? Parce que lors de chaque rendu, vous créez une toute nouvelle instance de fonction pour la prop ref : (node) => { ... }. Pendant son processus de réconciliation, React compare les props du rendu précédent à celles du rendu actuel. Il constate que la prop ref a changé (de l'ancienne instance de fonction à la nouvelle). Le contrat de React est clair : si le rappel ref change, il doit d'abord effacer l'ancienne ref en l'appelant avec null, puis définir la nouvelle en l'appelant avec le nœud DOM. Cela déclenche inutilement le cycle de nettoyage/configuration à chaque rendu.
Pour un simple console.log, c'est un impact mineur sur les performances. Mais imaginez que votre rappel fasse quelque chose de coûteux :
- Attacher et détacher des écouteurs d'événements complexes (par exemple,
scroll,resize). - Initialiser une bibliothèque tierce lourde (comme un graphique D3.js ou une bibliothèque de cartographie).
- Effectuer des mesures DOM qui provoquent des réorganisations de la mise en page.
L'exécution de cette logique à chaque mise à jour de l'état peut gravement dégrader les performances de votre application et introduire des bugs subtils et difficiles à tracer.
La solution : Mémoriser avec useCallback
La solution à ce problème est de s'assurer que React reçoit la même instance de fonction exacte pour le rappel ref lors des nouveaux rendus, à moins que nous ne voulions explicitement qu'elle change. C'est le cas d'utilisation parfait pour le hook useCallback.
useCallback renvoie une version mémorisée d'une fonction de rappel. Cette version mémorisée ne change que si l'une des dépendances de son tableau de dépendances change. En fournissant un tableau de dépendances vide ([]), nous pouvons créer une fonction stable qui persiste pendant toute la durée de vie du composant.
Refactorons notre exemple précédent en utilisant useCallback :
import React, { useState, useCallback } from 'react';
function OptimizedComponent() {
const [count, setCount] = useState(0);
// Create a stable callback function with useCallback
const myRefCallback = useCallback(node => {
// This logic now runs only when the component mounts and unmounts
console.log('Ref callback fired with:', node);
if (node !== null) {
// You can perform setup logic here
console.log('Element is mounted!');
}
}, []); // <-- Empty dependency array means the function is created only once
return (
<div>
<h3>Counter: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<div ref={myRefCallback}>
I am the referenced element.
</div>
</div>
);
}
Maintenant, lorsque vous exécutez cette version optimisée, vous ne verrez le journal de la console que deux fois au total :
- Une fois lorsque le composant est initialement monté (
Ref callback fired with: <div>...</div>). - Une fois lorsque le composant est démonté (
Ref callback fired with: null).
Cliquer sur le bouton "Increment" ne déclenchera plus le rappel ref. Nous avons réussi à empêcher le cycle de nettoyage/configuration inutile à chaque nouveau rendu. React voit la même instance de fonction pour la prop ref lors des rendus suivants et détermine correctement qu'aucun changement n'est nécessaire.
Scénarios avancés et meilleures pratiques
Bien qu'un tableau de dépendances vide soit courant, il existe des scénarios où votre rappel ref doit réagir aux changements de props ou d'état. C'est là que la puissance du tableau de dépendances de useCallback brille vraiment.
Gestion des dépendances dans votre rappel
Imaginez que vous devez exécuter une logique dans votre rappel ref qui dépend d'une partie de l'état ou d'une prop. Par exemple, définir un attribut data- basé sur le thème actuel.
function ThemedComponent({ theme }) {
const [internalState, setInternalState] = useState(0);
const themedRefCallback = useCallback(node => {
if (node !== null) {
// This callback now depends on the 'theme' prop
console.log(`Setting theme attribute to: ${theme}`);
node.setAttribute('data-theme', theme);
}
}, [theme]); // <-- Add 'theme' to the dependency array
return (
<div>
<p>Current Theme: {theme}</p>
<div ref={themedRefCallback}>This element's theme will update.</div>
{/* ... imagine a button here to change the parent's theme ... */}
</div>
);
}
Dans cet exemple, nous avons ajouté theme au tableau de dépendances de useCallback. Cela signifie :
- Une nouvelle fonction
themedRefCallbacksera créée uniquement lorsque la propthemechange. - Lorsque la prop
themechange, React détecte la nouvelle instance de fonction et réexécute le rappel ref (d'abord avecnull, puis avec l'élément). - Cela permet à notre effet - la définition de l'attribut
data-theme- de se réexécuter avec la valeurthememise à jour.
C'est le comportement correct et prévu. Nous disons explicitement à React de redéclencher la logique ref lorsque ses dépendances changent, tout en l'empêchant de s'exécuter lors de mises à jour d'état non liées.
Intégration avec des bibliothèques tierces
L'un des cas d'utilisation les plus puissants des rappels ref est l'initialisation et la destruction d'instances de bibliothèques tierces qui doivent s'attacher à un nœud DOM. Ce modèle exploite parfaitement la nature de montage/démontage du rappel.
Voici un modèle robuste pour la gestion d'une bibliothèque comme une bibliothèque de cartographie ou de graphiques :
import React, { useRef, useCallback, useEffect } from 'react';
import SomeChartingLibrary from 'some-charting-library';
function ChartComponent({ data }) {
// Use a ref to hold the library instance, not the DOM node
const chartInstance = useRef(null);
const chartContainerRef = useCallback(node => {
// The node is null when the component unmounts
if (node === null) {
if (chartInstance.current) {
console.log('Cleaning up chart instance...');
chartInstance.current.destroy(); // Cleanup method from the library
chartInstance.current = null;
}
return;
}
// The node exists, so we can initialize our chart
console.log('Initializing chart instance...');
const chart = new SomeChartingLibrary(node, {
// Configuration options
data: data,
});
chartInstance.current = chart;
}, [data]); // Re-create the chart if the data prop changes
return <div className="chart-container" ref={chartContainerRef} style={{ height: '400px' }} />;
}
Ce modèle est exceptionnellement propre et résilient :
- Initialisation : Lorsque le
divest monté, le rappel reçoit lenode. Il crée une nouvelle instance de la bibliothèque de graphiques et la stocke danschartInstance.current. - Nettoyage : Lorsque le composant est démonté (ou si
datachange, déclenchant une nouvelle exécution), le rappel est d'abord appelé avecnull. Le code vérifie si une instance de graphique existe et, si c'est le cas, appelle sa méthodedestroy(), empêchant ainsi les fuites de mémoire. - Mises à jour : En incluant
datadans le tableau de dépendances, nous nous assurons que si les données du graphique doivent être fondamentalement modifiées, l'ensemble du graphique est proprement détruit et réinitialisé avec les nouvelles données. Pour les mises à jour de données simples, une bibliothèque peut offrir une méthodeupdate(), qui pourrait être gérée dans unuseEffectdistinct.
Comparaison des performances : quand l'optimisation est-elle *vraiment* importante ?
Il est important d'aborder les performances avec un état d'esprit pragmatique. Bien qu'il soit bon d'envelopper chaque rappel ref dans useCallback, l'impact réel sur les performances varie considérablement en fonction du travail effectué à l'intérieur du rappel.
Scénarios d'impact négligeable
Si votre rappel n'effectue qu'une simple attribution de variable, la surcharge liée à la création d'une nouvelle fonction à chaque rendu est minuscule. Les moteurs JavaScript modernes sont incroyablement rapides en matière de création de fonctions et de collecte des ordures.
Exemple : ref={(node) => (myRef.current = node)}
Dans des cas comme celui-ci, bien que techniquement moins optimal, il est peu probable que vous mesuriez une différence de performance dans une application du monde réel. Ne tombez pas dans le piège de l'optimisation prématurée.
Scénarios d'impact significatif
Vous devez toujours utiliser useCallback lorsque votre rappel ref effectue l'une des opérations suivantes :
- Manipulation du DOM : Ajout ou suppression directe de classes, définition d'attributs ou mesure de la taille des éléments (ce qui peut déclencher une réorganisation de la mise en page).
- Écouteurs d'événements : Appeler
addEventListeneretremoveEventListener. Déclencher cela à chaque rendu est un moyen garanti d'introduire des bugs et des problèmes de performance. - Instanciation de la bibliothèque : Comme le montre notre exemple de graphique, l'initialisation et la suppression d'objets complexes sont coûteuses.
- Requêtes réseau : Faire un appel API basé sur l'existence d'un élément DOM.
- Passage de Refs aux enfants mémorisés : Si vous passez un rappel ref en tant que prop à un composant enfant enveloppé dans
React.memo, une fonction en ligne instable cassera la mémorisation et provoquera le rendu inutile du composant enfant.
Une bonne règle de base : Si votre rappel ref contient plus d'une simple affectation, mémorisez-le avec useCallback.
Conclusion : Écrire un code prévisible et performant
Le rappel ref de React est un outil puissant qui offre un contrôle précis sur les nœuds DOM et les instances de composants. Comprendre son cycle de vie - en particulier l'appel null intentionnel pendant le nettoyage - est la clé pour l'utiliser efficacement.
Nous avons appris que l'anti-modèle courant consistant à utiliser une fonction en ligne pour la prop ref conduit à des réexécutions inutiles et potentiellement coûteuses à chaque rendu. La solution est élégante et idiomatique à React : stabiliser la fonction de rappel à l'aide du hook useCallback.
En maîtrisant ce modèle, vous pouvez :
- Prévenir les goulots d'étranglement de performance : Éviter la logique de configuration et de suppression coûteuse à chaque changement d'état.
- Éliminer les bugs : S'assurer que les écouteurs d'événements et les instances de la bibliothèque sont gérés proprement sans doublons ni fuites de mémoire.
- Écrire un code prévisible : Créer des composants dont la logique ref se comporte exactement comme prévu, en ne s'exécutant que lorsque le composant est monté, démonté ou lorsque ses dépendances spécifiques changent.
La prochaine fois que vous utiliserez une ref pour résoudre un problème complexe, rappelez-vous la puissance d'un rappel mémorisé. C'est un petit changement dans votre code qui peut faire une différence significative dans la qualité et la performance de vos applications React, contribuant à une meilleure expérience pour les utilisateurs du monde entier.